package core;

import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
import java.util.TreeSet;

import core.exception.NotFoundException;
import core.reports.IReportVisitor;

/**
 * This class is a Singleton that manages all the products in the system. 
 */

public class ProductManager extends ModelObject 
{
	private static final long serialVersionUID = -5935227587905441791L;
	private static ProductManager _instance;
	private Set<Product> _products;
	private Map<Container, HashSet<Product>> _containerToProductMap;
	private Map<Product, HashSet<Container>> _productToContainerMap;
	
	private ProductManager()
	{
		//no preconditions
		_products = new TreeSet<Product>();
		_containerToProductMap = new HashMap<Container, HashSet<Product>>();
		_productToContainerMap = new HashMap<Product, HashSet<Container>>();
	}
	
	/**
	 * Get the single instance of ProductManager
	 * 
	 * @return returns the instance of ProductManager
	 */
	public static ProductManager instance()
	{
		//no preconditions
		if (_instance == null)
			_instance = new ProductManager();
		
		return _instance;
	}
	
	/**
	 * Sets the product manager to a difference product manager
	 * 
	 * @param newManager the new ProductManager to be used
	 */
	protected static void setInstance(ProductManager newManager)
	{
		//no preconditions
		_instance = newManager;
	}
	
	/**
	 * adds a product to the system
	 * 
	 * @param product the product to be added to the system
	 * 
	 * @pre canAddProduct(product)
	 */
	protected void addProduct(Product product)
	{
		assert(canAddProduct(product));
		_products.add(product);
		_productToContainerMap.put(product, new HashSet<Container>());
		addProductToContainer(product, RootStorageUnit.instance());
	}
	
	/**
	 * checks to see if a product can be added to the system
	 * 
	 * @param product product to check if can be added to the system
	 * 
	 * @return returns true if product can be added to the system, else returns false
	 * 				product can be added if not null and not already in system
	 */
	public boolean canAddProduct(Product product)
	{
		//no preconditions
		if (product == null)
			return false;
		//check if a product with bar code already exists in system
		if (containsProduct(product.getBarcode())) 
			return false;
		return true;
	}
	
	/**
	 * Gets the product that is associated with the bar code
	 * 
	 * @param barcode bar code of the desired product
	 * 
	 * @return returns the product with the bar code returns null if no product found
	 */
	public Product getProduct(Barcode barcode)
	{
		//no preconditions
		Product theProduct = null;
		for (Product p:_products)
		{
			if (p.getBarcode().equals(barcode))
			{
				theProduct = p;
				break;
			}
		}
		return theProduct;
	}
	
	/**
	 * Gets the product that is associated with the barcode
	 * 
	 * @param barcode string representation of the barcode of the desired product
	 * 
	 * @return returns the product with the barcode returns null if no product found
	 */
	public Product getProduct(String barcode){
		Barcode _barcode = new Barcode(barcode, BarcodeType.PRODUCT);
		return getProduct(_barcode);
	}
	
	/**
	 * checks if a product with the bar code exists in the system
	 * 
	 * @param barcode bar code of the product to be checked for
	 * 
	 * @return returns true if the product is found in the system, else returns false
	 */
	public boolean containsProduct(Barcode barcode)
	{
		//no preconditions
		if (getProduct(barcode) == null)
			return false;
		return true;
	}

	/**
	 * Gets an iterator for all the containers for a product
	 * 
	 * @param product the product that wish to get containers for
	 * 
	 * @return the iterator (not modifiable)
	 * 
	 * @throws NotFoundException if product is null or is not in the system
	 */
	public Iterator<Container> getContainersForProduct(Product product) throws NotFoundException
	{
		if(product == null)
			throw new NotFoundException("Product is null");
		if (!containsProduct(product.getBarcode()))
			throw new NotFoundException("Product not in System");
		Set<Container> containerSet = _productToContainerMap.get(product);
		return java.util.Collections.unmodifiableSet(containerSet).iterator();
	}

	/**
	 * Gets an iterator pointing to the products in a given container
	 * 
	 * @param container the specified container
	 * 
	 * @return iterator pointing to the products, returns null if no products in the container
	 * 
	 * @throws NotFoundException thrown if the container not associated with any products
	 */
	public Iterator<Product> getProductsForContainer(Container container) throws NotFoundException
	{
		//no preconditions
		Set<Product> mySet = _containerToProductMap.get(container);
		if (mySet == null)
			throw new NotFoundException("No products associated with the container");
		return java.util.Collections.unmodifiableSet(mySet).iterator();
	}

	/**
	 * Checks if the container and product are associated
	 * 
	 * @param product the product
	 * @param container the container
	 * 
 	 * @return returns true if product and container associated, else returns false
 	 * 		NOTE: returns false if the product is not in the system or null
	 */
	public boolean productContainsContainer(Product product, Container container)
	{
		if (product == null)
			return false;
		if (!containsProduct(product.getBarcode()))
			return false;
		return _productToContainerMap.get(product).contains(container);
	}
	
	/**
	 * Adds a product to a container
	 * 
	 * @param product the product
	 * @param container the container
	 * 
	 * @pre canAddProductToContainer(product, container)
	 */
	protected void addProductToContainer(Product product, Container container)
	{
		assert canAddProductToContainer(product, container);
		
		if (!(container instanceof RootStorageUnit)){
			// Check if an old container within storage unit has this product
			Container oldContainer = container.getStorageUnit().getContainerForProduct(product);
			if (oldContainer == container)
				return; // Don't do anything if product is already in this container.
			if (oldContainer != null) {
				// Tell item manager to reparent all items for given product and container
				ItemManager.instance().changeContainerForProduct(oldContainer, product, container);
				removeProductFromContainer(product, oldContainer);
			}		
		}
		
		if (!_containerToProductMap.containsKey(container))
		{
			_containerToProductMap.put(container, new HashSet<Product>());
		}
		_containerToProductMap.get(container).add(product);
		_productToContainerMap.get(product).add(container);
	}
	
	/**
	 * Checks whether can add a product to a container
	 * 
	 * @param product the product
	 * @param container the container
	 * 
	 * @return returns true if can add the product to container, else returns false
	 * 			can add product if it is not already in the container and it is in system
	 * 			NOTE: returns false if either parameter is null 
	 */
	public boolean canAddProductToContainer(Product product, Container container)
	{
		//no preconditions
		if (product == null || container == null)
			return false;
		if (!_products.contains(product))
			return false;
		Set<Product> products = _containerToProductMap.get(container);
		if (products == null)
			return true;
		return !products.contains(product);
	}
	
	/**
	 * Removes a product from the system
	 *  and any items belonging to this product
	 *  
	 * @param product product to be removed
	 * 
	 * @pre canRemoveProduct(product)
	 */
	protected void removeProduct(Product product)
	{
		assert canRemoveProduct(product);
		try
		{
			Iterator<Container> productsContainers = getContainersForProduct(product);
			for (Iterator<Container> iterator = productsContainers; iterator.hasNext();)
			{
				_containerToProductMap.get(iterator.next()).remove(product);
			}
		}
		catch(NotFoundException e)
		{
			//don't do anything if this exception thrown
		}
		finally
		{
			_productToContainerMap.remove(product);
			_products.remove(product);
			ItemManager.instance().removeAllItemsForProduct(product);
		}
	}
	
	/**
	 * checks whether can remove a product (product exists in the system)
	 * 
	 * @param product the product to be removed
	 * 
	 * @return returns true if can remove the product
	 */
	public boolean canRemoveProduct(Product product)
	{
		//no preconditions
		if (product == null)
			return false;
		if (_products.contains(product) && 
				ItemFacade.instance().getNumberOfItemsInProduct(
						product.getBarcode().toString()) == 0)
			return true;
		else
			return false;
	}
	
	/**
	 * Removes containers from all of the products 
	 * 
	 * @param containers a collection of containers to be removed
	 * 
	 * @pre containers != null
	 */
	protected void removeContainersFromAllProducts(Collection<Container> containers)
	{
		assert containers != null;
		for (Container c: containers)
		{
			Set<Product> products = _containerToProductMap.get(c);
			if (products != null)
			{
				for (Iterator<Product> iterator = products.iterator(); iterator.hasNext();)
				{
					_productToContainerMap.get(iterator.next()).remove(c);
				}
			}
			_containerToProductMap.remove(c);
		}	
	}
	
	/**
	 * Removes a product from a container
	 * 
	 * @param product the product
	 * @param container the container
	 * 
	 * @pre canRemoveProductFromContainer(product, container)
	 */
	protected void removeProductFromContainer(Product product, Container container)
	{
		assert canRemoveProductFromContainer(product, container);
		Set<Container> containers = _productToContainerMap.get(product);
		containers.remove(container);
		
		Set<Product> products = _containerToProductMap.get(container);
		products.remove(product);
	}
	
	/**
	 * Tests whether can remove a product from a container
	 * 
	 * @param product the product
	 * @param container the container
	 * @return returns true if can remove product from container, else returns false
	 * 			can remove if the product is in the container
	 */
	public boolean canRemoveProductFromContainer(Product product, Container container)
	{
		if (container instanceof RootStorageUnit)
			return canRemoveProduct(product);
		if (ItemManager.instance().numItemsInProductAndContainer(container, product) != 0)
			return false;
		if (product == null)
			return false;
		if (container == null)
			return false;
		Set<Container> set = _productToContainerMap.get(product);
		if (set == null)
			return false;
		return (_productToContainerMap.get(product).contains(container) ||
				(container instanceof RootStorageUnit));
	}
	
	/**
	 * Gets all the products in the system
	 * 
	 * @return iterator to a collection of the products
	 */
	protected Iterator<Product> getAllProducts()
	{	
		return  java.util.Collections.unmodifiableSet(_products).iterator();
	}
	
	/**
	 * Accepts a Visitor
	 * @param v the visiting IReportVisitor
	 */
	public void accept(IReportVisitor v) {
		Iterator<Product> iter = _products.iterator();
		if (iter == null)
			return;
		while (iter.hasNext())
		{
			iter.next().acceptRecursively(v);
		}
		v.visitProductManager(this);
	}
}
